﻿/*
 *	Copyright (c) 2009 Queensland University of Technology. All rights reserved.
 *	The QUT Bioinformatics Collection is open source software released under the 
 *	Microsoft Public License (Ms-PL): http://www.microsoft.com/opensource/licenses.mspx.
 */
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace QUT.Bio.Map2D {

	public partial class Map2DCanvas : UserControl, INotifyPropertyChanged, IPositioned {
		public event PropertyChangedEventHandler PropertyChanged;

		RectangleGeometry clipRectangle = new RectangleGeometry();

		private bool enableHorizontalZoom = true;
		private bool enableVerticalZoom = true;

		private double scale = 1.0;
		private double minimumScale = double.Epsilon;
		private double maximumScale = double.MaxValue;

		private double unitScaleFactor = 1.0;
		private ScaleTransform elementScale = new ScaleTransform();
		private ScaleTransform elementUnitScale = new ScaleTransform();
		private TranslateTransform displayCentre = new TranslateTransform();

		private TranslateTransform origin = new TranslateTransform();
		private ScaleTransform unitScale = new ScaleTransform();
		private ScaleTransform canvasScale = new ScaleTransform();
		private TransformGroup mapTransform = new TransformGroup();

		private Line xAxis = new Line();
		private Line yAxis = new Line();

		ScaleTransform strokeScale = new ScaleTransform();

		private List<PositionedElement> positionedElements = new List<PositionedElement>();
		private List<Edge> edges = new List<Edge>();
		private List<PositionedElement> shapes = new List<PositionedElement>();

		Status status = new Status();

		public Map2DCanvas () {
			InitializeComponent();

			layoutRoot.Clip = clipRectangle;

			Map.RenderTransformOrigin = new Point( 0.5, 0.5 );
			mapTransform.Children.Add( origin );
			mapTransform.Children.Add( unitScale );
			mapTransform.Children.Add( canvasScale );
			Map.RenderTransform = mapTransform;

			strokeScale.ScaleX = 1;
			strokeScale.ScaleY = 1;

			//underlay.Children.Add( xAxis );
			//xAxis.StrokeThickness = 1;
			//xAxis.Stroke = new SolidColorBrush( Color.FromArgb( 255, 200, 200, 200 ) );

			//underlay.Children.Add( yAxis );
			//yAxis.StrokeThickness = 1;
			//yAxis.Stroke = xAxis.Stroke;

			this.SizeChanged += new SizeChangedEventHandler( Resized );
			layoutRoot.MouseLeftButtonDown += MouseLeftButtonDown;
			layoutRoot.MouseEnter += MouseEnter;
			layoutRoot.MouseLeave += MouseLeave;

			//overlay.Children.Add( status );
			//status.Visibility = Visibility.Collapsed;

			PropertyChanged += UpdateStatus;
		}

		#region Property: EnableHorizontalZoom
		/// <summary>
		/// Get or set the horizontal zoom capability.
		/// <para>On setting, the display Zoom()s to reflect the new state.</para>
		/// </summary>

		public bool EnableHorizontalZoom {
			get {
				return enableHorizontalZoom;
			}
			set {
				enableHorizontalZoom = value;
				Zoom();
			}
		}
		#endregion

		#region Property: EnableVerticalZoom
		/// <summary>
		/// Get or set the vertical zoom capability.
		/// <para>On setting, the display Zoom()s to reflect the new state.</para>
		/// </summary>

		public bool EnableVerticalZoom {
			get {
				return enableVerticalZoom;
			}
			set {
				enableVerticalZoom = value;
				Zoom();
			}
		}
		#endregion

		private void UpdateStatus ( object sender, PropertyChangedEventArgs args ) {
			switch ( args.PropertyName ) {
				case "X":
					status.X = origin.X;
					break;
				case "Y":
					status.Y = origin.Y;
					break;
				case "Scale":
					status.Scale = scale;
					break;
				default:
					break;
			}
		}

		public bool ShowStatus {
			get {
				return status.Visibility == Visibility.Visible;
			}
			set {
				status.Visibility = value ? Visibility.Visible : Visibility.Collapsed;
			}
		}

		/// <summary>
		/// Creates a new LabelledVertex in a square box centred on (x,y) with 
		/// the specified radius; adds the vertex to the map.
		/// </summary>
		/// <param name="glyph">The glyph to display for a vertex.</param>
		/// <param name="label">The textual label to display.</param>
		/// <param name="x">The horizontal location ofthe centre of the icon.</param>
		/// <param name="y">The vertical location ofthe centre of the icon.</param>
		/// <returns>The resulting PositionedElement.</returns>

		public PositionedElement AddVertex (
			FrameworkElement glyph,
			string label,
			double x,
			double y
		) {
			LabelledVertex v = new LabelledVertex( glyph );
			double xOffset = v.Glyph.Width / 2;
			double yOffset = v.Glyph.Height / 2;

			v.Label = label;

			return AddAnchorBottomRightElement( v, x, y, xOffset, yOffset, true, false );
		}

		/// <summary>
		/// Creates a new elliptical LabelledVertex in a square box centred on (x,y) with 
		/// the specified radius; adds the vertex to the map.
		/// </summary>
		/// <param name="label">The textual label to display.</param>
		/// <param name="x">The horizontal location ofthe centre of the icon.</param>
		/// <param name="y">The vertical location ofthe centre of the icon.</param>
		/// <param name="radius">The radius of the icon.</param>
		/// <param name="fill">The fill colour.</param>
		/// <param name="stroke">The fill colour.</param>
		/// <returns>The resulting PositionedElement.</returns>

		public PositionedElement AddVertex (
			string label,
			double x,
			double y,
			double radius,
			Brush fill,
			Brush stroke
		) {
			return AddVertex(
				new Ellipse {
					Width = radius * 2,
					Height = radius * 2,
					Fill = fill,
					Stroke = stroke
				},
				label,
				x,
				y
			);
		}

		public PositionedElement AddAnchorBottomRightElement (
			FrameworkElement element,
			double x,
			double y,
			double offsetX,
			double offsetY,
			bool preserveElementSize,
			bool preserveStrokeThickness
		) {
			AnchorBottomRightElement anchoredElement = new AnchorBottomRightElement(
				element,
				x,
				y,
				offsetX,
				offsetY,
				preserveElementSize ? elementUnitScale : null,
				preserveElementSize ? elementScale : null,
				displayCentre,
				preserveStrokeThickness
			);
			lock ( positionedElements ) {
				positionedElements.Add( anchoredElement );
				nodeCanvas.Children.Add( element );
			}
			return anchoredElement;
		}

		public PositionedElement AddCentredElement (
			FrameworkElement element,
			double x,
			double y,
			bool preserveElementSize,
			bool preserveStrokeThickness
		) {
			CentredElement centredElement = new CentredElement(
				element,
				x,
				y,
				preserveElementSize ? elementUnitScale : null,
				preserveElementSize ? elementScale : null,
				displayCentre,
				preserveStrokeThickness
			);
			lock ( positionedElements ) {
				positionedElements.Add( centredElement );
				nodeCanvas.Children.Add( element );
			}
			return centredElement;
		}

		public PositionedElement Add (
			PositionedElement positionedElement
		) {
			lock ( positionedElements ) {
				positionedElements.Add( positionedElement );
				FrameworkElement element = positionedElement.Element;
				nodeCanvas.Children.Add( element );

				if ( positionedElement.StrokeThickness != double.NaN && element is Shape ) {
					shapes.Add( positionedElement );
				}
			}

			return positionedElement;
		}

		public PositionedElement Add (
			FrameworkElement element,
			double x,
			double y,
			double offsetX,
			double offsetY,
			bool preserveElementSize,
			bool preserveStrokeThickness
		) {
			PositionedElement positionedElement = new PositionedElement(
				element,
				x,
				y,
				offsetX,
				offsetY,
				preserveElementSize ? elementUnitScale : null,
				preserveElementSize ? elementScale : null,
				displayCentre,
				preserveStrokeThickness
			);
			Add( positionedElement );
			return positionedElement;
		}

		public void Remove (
			PositionedElement positionedElement
		) {
			lock ( positionedElements ) {
				nodeCanvas.Children.Remove( positionedElement.Element );
				positionedElements.Remove( positionedElement );

				if ( positionedElement.StrokeThickness != double.NaN ) {
					shapes.Remove( positionedElement );
				}
			}
		}

		public void ClearPositionedElements () {
			lock ( positionedElements ) {
				positionedElements.Clear();
				nodeCanvas.Children.Clear();
				shapes.Clear();
			}
		}

		public Edge AddEdge (
			double x1,
			double y1,
			double x2,
			double y2,
			double thickness,
			Brush brush
		) {
			Edge edge = new Edge(
				x1,
				y1,
				x2,
				y2,
				thickness,
				brush,
				strokeScale,
				displayCentre
			);
			lock ( edges ) {
				edges.Add( edge );
				edgeCanvas.Children.Add( edge.Element );
			}
			return edge;
		}

		public void Remove ( Edge edge ) {
			lock ( edges ) {
				edges.Remove( edge );
				edgeCanvas.Children.Remove( edge.Element );
			}
		}

		public void ClearEdges () {
			lock ( edges ) {
				edges.Clear();
			}
		}

		void Resized ( object sender, SizeChangedEventArgs e ) {
			double xScaleFactor = layoutRoot.ActualWidth / 2;
			double yScaleFactor = layoutRoot.ActualHeight / 2;

			unitScaleFactor = Math.Min( xScaleFactor, yScaleFactor );

			if ( enableHorizontalZoom ) {
				unitScale.ScaleX = unitScaleFactor;
				elementUnitScale.ScaleX = 1.0 / unitScaleFactor;
				strokeScale.ScaleX = 1.0 / unitScaleFactor;
			}
			else {
				unitScale.ScaleX = xScaleFactor;
				elementUnitScale.ScaleX = 1.0 / xScaleFactor;
				strokeScale.ScaleX = 1.0 / xScaleFactor;
			}

			if ( enableVerticalZoom ) {
				unitScale.ScaleY = unitScaleFactor;
				elementUnitScale.ScaleY = 1.0 / unitScaleFactor;
				strokeScale.ScaleY = 1.0;
			}
			else {
				unitScale.ScaleY = yScaleFactor;
				elementUnitScale.ScaleY = 1.0 / yScaleFactor;
				strokeScale.ScaleY = 1.0;
			}

			xAxis.X1 = 0;
			xAxis.Y1 = yScaleFactor;
			xAxis.X2 = layoutRoot.ActualWidth - 1;
			xAxis.Y2 = yScaleFactor;

			yAxis.X1 = xScaleFactor;
			yAxis.Y1 = 0;
			yAxis.X2 = xScaleFactor;
			yAxis.Y2 = layoutRoot.ActualHeight - 1;

			clipRectangle.Rect = new Rect( 0, 0, ActualWidth, ActualHeight );

			displayCentre.X = xScaleFactor;
			displayCentre.Y = yScaleFactor;

			RescaleStrokeThickness();
		}

		private void RescaleStrokeThickness () {
			foreach ( PositionedElement element in shapes ) {
				if ( !double.IsNaN( element.StrokeThickness ) ) {
					Shape shape = (Shape) element.Element;
					shape.StrokeThickness = element.StrokeThickness / scale / unitScaleFactor;
				}
			}
		}

		public void Pan ( double dx, double dy ) {
			X = origin.X + dx / ScaleX / unitScaleFactor;
			Y = origin.Y + dy / ScaleY / unitScaleFactor;
		}

		public void ZoomIn () {
			Scale *= Math.Pow( 2, 0.25 );
		}

		public void ZoomOut () {
			Scale /= Math.Pow( 2, 0.25 );
		}

		private void Zoom () {
			canvasScale.ScaleX = ScaleX;
			canvasScale.ScaleY = ScaleY;
			elementScale.ScaleX = 1.0 / ScaleX;
			elementScale.ScaleY = 1.0 / ScaleY;
			xAxis.StrokeThickness = 1.0 / ScaleX;
			yAxis.StrokeThickness = 1.0 / ScaleY;
			strokeScale.ScaleX = 1.0 / scale / unitScaleFactor;
			strokeScale.ScaleY = 1.0;
			status.Scale = scale;
			RescaleStrokeThickness();
		}

		private Point oldMousePos = new Point();

		private new void MouseEnter ( object sender, MouseEventArgs args ) {
#if ! SILVERLIGHT
			MouseWheel += Map2DCanvas_MouseWheel;
#else
			QUT.Bio.Map2D.MouseWheel.Callback += MouseWheelCallback;
#endif
		}

		private new void MouseLeave ( object sender, MouseEventArgs args ) {
#if ! SILVERLIGHT
			MouseWheel -= Map2DCanvas_MouseWheel;
#else
			QUT.Bio.Map2D.MouseWheel.Callback -= MouseWheelCallback;
#endif
			layoutRoot.MouseLeftButtonUp -= MouseLeftButtonUp;
			layoutRoot.MouseMove -= MouseMove;
			layoutRoot.ReleaseMouseCapture();
		}

		private new void MouseMove ( object sender, MouseEventArgs args ) {
			Point newMousePos = args.GetPosition( null );

			double dx = newMousePos.X - oldMousePos.X;
			double dy = newMousePos.Y - oldMousePos.Y;

			Pan( dx, dy );

			oldMousePos.X = newMousePos.X;
			oldMousePos.Y = newMousePos.Y;
		}

		private new void MouseLeftButtonUp ( object sender, MouseButtonEventArgs args ) {
			args.Handled = true;
			layoutRoot.MouseLeftButtonUp -= MouseLeftButtonUp;
			layoutRoot.MouseMove -= MouseMove;
			layoutRoot.ReleaseMouseCapture();
		}

		private new void MouseLeftButtonDown ( object sender, MouseButtonEventArgs args ) {
			args.Handled = true;
			oldMousePos = args.GetPosition( null );
			layoutRoot.CaptureMouse();
			layoutRoot.MouseLeftButtonUp += MouseLeftButtonUp;
			layoutRoot.MouseMove += MouseMove;
		}

#if ! SILVERLIGHT
		void Map2DCanvas_MouseWheel ( object sender, MouseWheelEventArgs e ) {
			MouseWheelCallback( e.Delta );
		}
#endif

		private void MouseWheelCallback ( double delta ) {
			if ( delta > 0 ) {
				ZoomIn();
			}
			else if ( delta < 0 ) {
				ZoomOut();
			}
		}

		public double X {
			get {
				return origin.X;
			}
			set {
				if ( double.IsInfinity( value ) || double.IsNaN( value ) ) {
					throw new ArgumentException();
				}

				if ( value != origin.X ) {
					// Debug.WriteLine( String.Format( "X = {0}", X ) );
					origin.X = value;
					NotifyPropertyChanged( "X" );
				}
			}
		}

		public double Y {
			get {
				return origin.Y;
			}
			set {
				if ( double.IsInfinity( value ) || double.IsNaN( value ) ) {
					throw new ArgumentException();
				}

				if ( value != origin.Y ) {
					// Debug.WriteLine( String.Format( "Y = {0}", Y ) );
					origin.Y = value;
					NotifyPropertyChanged( "Y" );
				}
			}
		}

		/// <summary>
		/// Return the horizontal scale factor for the canvas.
		/// </summary>
		private double ScaleX {
			get {
				return enableHorizontalZoom ? scale : 1.0;
			}
		}

		/// <summary>
		/// Return the vertical scale factor for the canvas.
		/// </summary>
		private double ScaleY {
			get {
				return enableVerticalZoom ? scale : 1.0;
			}
		}

		private void NotifyPropertyChanged ( string propertyName ) {
			if ( PropertyChanged != null ) {
				PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
			}
		}

		public double MinimumScale {
			get {
				return minimumScale;
			}
			set {
				if ( value <= 0 || double.IsInfinity( value ) || double.IsNaN( value ) || value > maximumScale ) {
					throw new ArgumentException();
				}

				if ( minimumScale != value ) {
					minimumScale = value;
					NotifyPropertyChanged( "MinimumScale" );
				}
			}
		}

		public double MaximumScale {
			get {
				return maximumScale;
			}
			set {
				if ( value <= 0 || double.IsInfinity( value ) || double.IsNaN( value ) || value < minimumScale ) {
					throw new ArgumentException();
				}

				if ( maximumScale != value ) {
					maximumScale = value;
					NotifyPropertyChanged( "MaximumScale" );
				}
			}
		}

		public Brush LayoutBackground {
			get {
				return layoutRoot.Background;
			}
			set {
				layoutRoot.Background = value;
			}
		}

		public double Scale {
			get {
				return (double) GetValue( ScaleProperty );
			}
			set {
				SetValue( ScaleProperty, value );
			}
		}

		// Using a DependencyProperty as the backing store for Scale.  This enables animation, styling, binding, etc...
		public static readonly DependencyProperty ScaleProperty = DependencyProperty.Register( 
			"Scale", 
			typeof( double ), 
			typeof( Map2DCanvas ), 
			new PropertyMetadata( 1.0, ScaleChanged ) );


		private static void ScaleChanged ( DependencyObject d, DependencyPropertyChangedEventArgs e ) {
			double newValue = (double) e.NewValue;
			double oldValue = (double) e.OldValue;
	
			Map2DCanvas canvas = (Map2DCanvas) d;

			if ( newValue <= 0 || double.IsInfinity( newValue ) || double.IsNaN( newValue ) ) {
				throw new ArgumentException( "Value must be finite and positive positive." );
			}

			double v = newValue;

			if ( v < canvas.minimumScale ) {
				v = canvas.minimumScale;
			}

			if ( v > canvas.maximumScale ) {
				v = canvas.maximumScale;
			}

			if ( canvas.scale != v ) {
				canvas.scale = v;
				canvas.Zoom();
				canvas.NotifyPropertyChanged( "Scale" );
			}
		}

		#region Property: Background.
		/// <summary>
		/// Get or set the background brush for this control.
		/// </summary>

		public new Brush Background {
			get {
				return layoutRoot.Background;
			}
			set {
				layoutRoot.Background = value;
			}
		} 
		#endregion
	}
}
